Advanced L2.6KM rootkit developmentAdvanced L2.6KM rootkit development
Pablo Fernandez
Rootkit installation is the phase that divides a
compromised computer from an "owned" computer. This article will focus
on the development of a rootkit for the 2.6 series of the Linux Kernel.
Techniques and methods of hiding the attacker actions within the system
will be the primary target, along with discussing how to detect
rootkits in the owned box: know your enemy, know thyself.
Knowledge about the internals of rootkits has an enormous
value from different points of view; an attacker doesn't really own a
system until a proper way to control the entire system has been taken
care, a system administrator needs to know how they work in order to be
able to evaluate if a system has been compromised.
This article will describe the most important techniques
used in a real life extensible LKM rootkit called SIDE, built for the
current 2.6 series of the Linux Kernel. In next articles more features
will be added to the rootkit.
Hiding the module
Since the rootkit will run within the system as a kernel
module, proper care has to be taken so that it's not found with
commands such as lsmod or through /proc/modules. Code in listing 1
takes care of this. In order to properly understand how this works,
it's important to comprehend how modules are ordered within the kernel
and the theory behind this hiding technique (see the Modules frame).
Listing 1. Hiding the module
lock_kernel(); __this_module.list.prev->next = __this_module.list.next;
__this_module.list.next->prev = __this_module.list.prev;
__this_module.list.prev = LIST_POISON1; __this_module.list.next = LIST_POISON2; unlock_kernel();
Basically, through this code the module detaches itself
from the kernel's internal circular double-linked list of loaded
modules.
Hiding processes
The ability to hide processes from every user in the
system (including root) is one of the basic futures a rootkit should
implement.
User space tools (such as ps(1) or top(1)) know about
tasks (processes) reading the /proc directory. Each task running in a
system creates an entry in the form /proc/, where useful
information about that process can be obtained. What user space tools
do is open the /proc directory and query the existence of
/proc/, where 1 <= n <= pid_max, if the directory does
not exist that PID is assumed to be free, whereas if the directory does
exist information can be gathered from it.
With this assumption in mind and the knowledge about the
internals of VFS (see VFS internals frame) it's possible to make user
space tools believe existent PIDs are actually free. This is done by
interrupting the readdir call in the VFS layer. To accomplish this, the
table that contains the address of the readdir call has to be modified
with the address of the new segment of code that reimplements this
function. The porpoise of interrupting readdir is so the filldir
argument can be modified to point to a different implementation of
filldir, which will discard those directories that identify hidden PIDs.
Listing 2. Fragment of /proc's filldir reimplementation
if (!(process = _atoi(name, &process)));
else if (!process_is_authed(current) && process_is_hidden(process))
return 0;
if (p_proc_filldir)
return p_proc_filldir(buf, name, nlen, off, ino, x);
Rootkit detectors
Rootkit detectors use a technique to find hidden processes that consist in sending a SIGCONT signal to all possible processes.
Signals to processes are sent through the kill(2) system
call. When a signal is sent to a process that doesn't exist, kill(2)
returns the -1 value and errno is set to ESRCH, while when a process
does exist kill(2) returns 0.
This way, rootkit detectors realize if processes exists
or not without using /proc's data. After this, the list created is
compared with the list of processes that /proc shows. If a difference
is found between both lists it means that a process is hidden from user
space.
From the rootkit point of view, fooling the rootkit
detector that relies on this technique is a simple matter of extending
the code. All that is required is that the rootkit intercepts the
kill(2) system call (see frame System calls) and if a signal is sent
from someone else that is not the superroot user (see Superroot frame)
to a process which is in hidden state the new callback function should
return ESRCH, but if that's the case the original kill(2) function
should be called and it's return value returned.
Listing 3. sys_kill() replacement
asmlinkage int new_kill(pid_t pid, int sig) { struct siginfo info = { .si_signo = sig,
.si_errno = 0, .si_code = SI_USER,
.si_pid = current->tgid, .si_uid = current->uid };
if (!process_is_hidden(pid) || process_is_authed(current)) return kill_proc(pid, sig, &info);
return -ESRCH;
}
Aftermath: The rootkit detector technique
There are several ways for a rootkit detector to
recognize rootkits. Since user space tools can be easily fooled, there
is no point in keeping rootkit detectors in it. Once in kernel space
checking for sys_call_table modifications it's as easy as it can get.
Also checking for hidden processes would be easier, since they can't be
unattached from the processes list as easy as modules; their presence
in such list is the only warranty that they will get execution time
slices.
While some rootkit detectors do use some of this
techniques, most of them stay in user space. The lesson for paranoid
administrators is not to blindly rely on rootkit detectors.
Hiding network connections
User space programs and applications know about ongoing
network traffic through the /proc/net entries, within that location
several entries (that look like files, but are actually proc_dir_entry
structures), like tcp and tcp6 (if CONFIG_IPV6 is enabled). This
entries contain the information of networking that's going on in the
system.
Using a different method read calls can be intercepted
too, thus the information returned can be modified in transit. While
the network connection is still active (or a socket is in LISTEN state)
netstat and tools alike won't be able to see it.
SIDE provides a very nice way of hiding network
connections, very similar (although not remotely as powerful) as
Netfilter. A list of conditions and commands is defined during runtime
(see Table 1) and when information about sockets is requested the list
is matched with each socket, if some rule applies the command
associated with the condition is executed. The command can be either to
show or hide the socket from user space. This list is pretty powerful.
Default actions can be defined with the all condition at the end of the
list, which can be fully manipulated during runtime (and it can even
execute default commands when the module is loaded).
The method used to hide network connections consists in
grabing a handle to the proc_dir_entry of the protocol that needs to be
intercepted, such as tcp. proc_dir_entryies belonging to the /proc/net
space can be found through the circular double linked list in
proc_net->subdir. Iterating through it checking for the correct name
in the node->name member should get it right.
Once this structure has been grabbed, the seq_show
pointer needs to be overwritten with a new implementation of this
function. SIDE's implementation fetches the correct data (using the
original function) and matches each line of the data with the loaded
rules, applying specified actions in the matching lines.
The problematic
Due to the nature of network traffic it's impossible to
really hide activity from the eyes of a wise administrator. There are
usually several hops between the compromised system and the other end
of the hidden communication. These hops will clearly show this hidden
traffic and there's little to do about it.
In fact, even when a connection is not yet established,
if a socket in LISTEN state is hidden, nmap will reveal that there's an
open port which netstat doesn't show. Certainly something that can be
done to avoid this problem is using Port Knocking (hakin9 6/2005), but
once the connection is opened and traffic is on the way it will be easy
to detect this traffic (Removing spiderwebs - detection of illegal
connection sharing, hakin9 3/2005).
A neat trick would be not to establish a connection at
all, exchanging packets of information in ICMP or UDP packets is an
interesting way of controlling a system and to get information about
it's state. This way the attacker can even control the compromised
system without leaving any trails, using spoofed UDP or ICMP packets.
The present rootkit will be expanded in subsequent
articles that will enhance it with many more features, including those
mentioned here.
Hiding files
A system is often compromised to be used as a safety
platform from where to launch [D]DoS attacks, or to be used as a hop
between the attacker and another compromised system. Most of the time
the attacker will most likely need to upload some files to the system
to perform these attacks. Of course, if such tools are found by the
administrator, questions will arise. Thus, a rootkit should always be
able to hide files in the system, again, from any user, including root.
Again, we are going to use the knowledge of VFS to perform such actions, using the exact same method used to hide processes.
This time the fs object will store a list of hidden
filenames. This filenames lack a path, so any file hidden in a
directory will imply hiding every file with the same filename in all
directories. The reason for this feature is to force the superroot user
to use non standard names since there are still other methods that are
not being handled, for example, even though hidden files are not going
to be listed in directories they are still accessible through system
calls such as open(2), stat(2), etc. SIDE does not currently support
hiding files from this methods, although all it's required is to
replace those system calls (and a few more such as rename(2)).
Suggestion: it would be a nice exercise to expand this features when
you finish reading this article.
Note that the methodology used is filesystem dependant.
If hiding files in different mount points is desired SIDE has to be
modified in the vfs.c file to include hiding from those mount points.
It's completely safe to use the same readdir and filldir as the root
filesystem uses.
The problematic
Again, the action of hiding files has an intrinsic problematic on it's own, similar to the problem with network connections.
Files are stored in disks, disks that can be accessed in
different ways, like a rescue CD-ROM where the user mounts the root
partition. Since the rootkit isn't loaded in the running kernel, the
hidden files are not hidden any more. There are different ways to make
it a little harder to find those hidden files. The easiest and most
frequent protection is security by obscurity; storing files in
non-standard places with non-descriptive and confusing names.
Another and much better approach is storing all the files
in a loopback filesystem, of course, given this methodology proper care
should be taken so that such mounted filesystem is seen neither with
mount(8) nor through /proc/mounts. This methodology also allows easily
encryption of the filesystem, so that even if the administrators are
suspicious, they will never get to see what's inside of the filesystem.
Normal user with root permissions
The superroot user is identified by a special non-zero
UID and GID, thus the superroot user lacks root permissions, of course,
this is absolutely unacceptable. That's why SIDE implements a mechanism
to establish an UID of 0 to every process the superroot executes.
This is also done in the interrupted call to the
lookup() function in the /proc directory. Whenever an authenticated
process (see the Superroot frame) access anything in it, it's UID and
other related values are set to 0 (root), and some capabilities are
fully activated (cap_effective, cap_inheritable and cap_permitted). If
the process does not access anything in /proc it will run with the
Superroot UID, that's why some extremely little and simple programs
will not identify themselves as root, such as the whoami command.
Runtime usability
SIDE provides a very comfortable interface during
runtime. In vfs.c, the rootkit intercepts lookup() calls in the /proc
filesystem. This way, the superroot user (see Superroot frame) can
interact with the rootkit, to make it, for example, hide a process or
to grant root (or superroot) permissions to some user. Sending commands
to the rootkit it's pretty easy, all the user has to do is try to
access a file in the /proc filesystem. The name of the file will be
interpreted as the command.
SIDE organizes commands by objects, thus, depending on
what the user wants to do, commands are executed against specific
objects.
Currently SIDE recognizes three different objects: net,
for manipulation of the network list; sys, to handle process and user
related properties; fs, for manipulation of the hidden files list.
There are several commands for this objects. For brevity
sake a few of them will be listed in Table 1. The rest of them can be
found in the COMMANDS.txt file within the package.
Commands should be executed like the following:
echo > /proc/[object].[command[=args]]
Modules
Loaded modules are stored within the kernel in a
circular double-linked list, where each node of the list represents a
struct module (defined in include/module.h). Modules are usually
gathered reading the /proc/modules entry. The list is of modules is
created by m_show, in kernel/module.c, going through the previously
named linked list.
The fact that this list is accessible from each module
allows it modification and manipulation. The tecnhnique used to hide
the module is to detach the module node from the linked list,
connecting directly the previous and next value within the list with
themselves.
This technique has been covered in-depth by a very interesting article written by Mariusz Burdach in hakin9 3/2005.
System calls
System calls are the interface that reside in the kernel,
which communicate user space with the kernel. Everything that goes from
the kernel to the user space or vice versa has to go through a system
call. This is the main reason rootkits had always been specially
interested in intercepting them, since controlling them translates into
controlling what the user sees and what the user can do.
There are many system calls, like open(2), read(2), etc.
All this functions are referenced by pointers in an array called System
Calls Table, better known as sys_call_table.
Historically modifying the sys_call_table was a matter of
just overwriting the desired pointer with a new memory address with the
new function, this way any system call (except for execve(2) which
needs to be handled more carefully) could be easily intercepted.
Figure 1. Modification of sys_call_table
(Un)fortunately, since 2.5.41 Linux does not export the
sys_call_table symbol any more, while it still exists the memory
address is no longer available to modules.
To find the correct address there's a technique that can
be used: /usr/src/linux/include/asm/unistd.h lists the order of the
sys_call_table with __NR constants that define the offset in the array
where each system call is, and since the address of each system call is
known (every system call is exported), memory can be scanned looking
for the sys_call_table.
The code that does this simply declares a pointer that
starts at a low memory address (usually the address of loops_per_jiffy
is used) and loops until an __NR offset of the pointer matches the
correct address of the same system call. If a high memory address is
reached (like boot_cpu_data), something wrong happened (maybe a module
is already intercepting the system call which is being used to look the
sys_call_table), which means the system call table could not be found.
If the system call table is not found, interruptions of system calls
wouldn't be possible. Note that this is completely independent of the
VFS interruptions.
Listing 4. Search for sys_call_table
unsigned long ptr;
extern int loops_per_jiffy;
for (ptr = (unsigned long) &loops_per_jiffy; ptr < (unsigned long)&boot_cpu_data; ptr += sizeof(void *)) { unsigned long *p;
p = (unsigned long *)ptr;
if (p[__NR_close] == (u32) sys_close) return (u32 **) p;
}
Once sys_call_table has been found altering it is as
simple as it was in the past, see Listing 3 for an example of how to
replace the open(2) system call.
Listing 5. Intercepting a system call
u32 **sys_call_table;
asmlinkage int (*old_open)(const char *, int, mode_t);
if ((sys_call_table = find_sys_call_table())) { old_open = (void*) sys_call_table[__NR_OPEN];
sys_call_table[__NR_OPEN] = (u32*) new_open;
}
It's important to keep
in mind that system calls are very critical to a system. If a system
call is intercepted and the new callback function (in Listing 3, new_open)
doesn't behaves correctly the system will misbehave too, and most
probably become completely unstable. A common practice is to call the
original system call from the new callback function when the new
callback function decides to allow it's execution. That's exactly the
reason code in Listing 3 saves a pointer to the original function,
also, when the module is to be unloaded the sys_call_table should be modified to point to the original location.
VFS Internals
The Virtual File System or Virtual Filesystem Switch is
a layer between file-related system calls (like open(2)) and the actual
filesystem implementations (like ext2, ext3, reiserfs, jfs, etc.). It provides a common interface to ease the work of filesystem implementators.
Filesystem implementations have to define a set of
predefined functions and methods, and notify the VFS layer about those
methods, which are invoked by it as callback functions through function
pointers. Basically VFS has an structure that each filesystem has to
fill and register in the VFS layer. In that structure it is given the
required information to find the addresses where those callback
functions are to be found.
Throughout the development of a rootkit, the most
interesting call will undoubtedly be readdir. This callback function
provides the algorithm to call the function parameter filldir, which
will be called for each file or directory read by readdir. Its return
value is used to prepare the information about the directory read.
Throughout this article a technique is going to be
heavily used which uses the return value 0 in the filldir function.
This return value causes readdir to discard the information of the
readed item.
Figure 2. The VFS layer
Superroot
If any user of the system was able to control a rootkit
that has the ability to bring the system down to it knees, it wouldn't
be a very good one. Before SIDE executes any command users must
authenticate themselves to the rootkit, this is done with the
sys.superroot command. For this command to successfully authenticate
the user, the key has to be specified as a parameter of the command.
The key is a (usually) random string that identifies the
installation. SIDE selects the key for each installation when the
configure script is run.
When the correct key is given the UID and GID of the
user is changed to those that identify the superroot user (selected at
configure time as well).
The superroot user is required to allow the execution of
commands and to avoid hiding information from that particular user that
is hidden to other users.
Table 1. Command list
|
Command
|
Example
|
Description
|
|
net.hide.src=[IP]
|
net.hide.src=192.168.0.10
|
Hide network connections where the local address is [IP]
|
|
net.show.dstport=[PORT]
|
net.show.dstport=22
|
Show all network connections where the remote port is [PORT]
|
|
sys.superroot=[KEY]
|
sys.superroot=dSi2d_q@d
|
Get superroot permissions if the key [KEY] is correct
|
|
sys.hide=[PID]
|
sys.hide=1
|
Hide process with PID [PID]
|
|
sys.show=[PID]
|
sys.show=5982
|
Show hidden process with PID [PID]
|
|
sys.guid=[UID],[GID]
|
sys.guid=1000,1000
|
Drop superroot, set UID [UID] and GID [GID]
|
|
fs.hide=[FILENAME]
|
fs.hide=dfdfdf-nc
|
Hide files named [FILENAME]
|
|
fs.show=[FILENAME]
|
fs.show=dfdfdfdf-arpspoof
|
Show hidden files named [FILENAME]
|
Conclusion
Throughout this article different techniques and
approaches were studied on the subject of rootkit development in the
2.6 series of the Linux Kernel. Methodologies on how to hide network
connections, processes, modules and files were reviewed and counter
measures that rootkit detectors use, as well as new counter measures
that should be put in practice by rootkit detectors developers and
administrators. Yes.